Skip to content

feat(factors): reproduce valley/ridge VWAP-ratio factor (PR-J, both axes PASS but 4.4x turnover kills tradability) - #70

Merged
StackOverFlow11 merged 2 commits into
mainfrom
feat/pr-j-valley-ridge-vwap-ratio
Jul 20, 2026
Merged

feat(factors): reproduce valley/ridge VWAP-ratio factor (PR-J, both axes PASS but 4.4x turnover kills tradability)#70
StackOverFlow11 merged 2 commits into
mainfrom
feat/pr-j-valley-ridge-vwap-ratio

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

Reproduce the valley/ridge weighted-price-ratio factor — the fourth factor of Kaiyuan Securities microstructure #27 (loop reproduction 7/10). It swaps PR-I's denominator (all-visible VWAP) for the ridge VWAP, contrasting the two behavioural groups directly.

Result: both assessed axes PASS (Watch/Watch) — IC +0.0339, ICIR 0.425 (CI low 0.355 > the 0.30 bar), NW-t 14.11, win rate 0.668, monotonicity 0.90, N_eff 854; incremental ICIR CI low +0.227 > 0.15. Prototype (persisted at tmp/design/pr_j_prototype_sanity.txt, run before the full run, same seed/names as PR-I for comparability): mean RankIC +0.0438, PASS.

What this establishes: the price-level signal survives the denominator swap, so it is a property of the family rather than an artifact of PR-I's specific denominator. Combined with the earlier two: from this one report, count-based (peak-minute count) is weakly positive, timing-based (peak-interval kurtosis) is null, and both price-level statistics pass.

But tradability collapses, and the mechanism is measured. Head-to-head against PR-I on the identical cell: gross long-short spread is essentially the same (+0.000396 vs +0.000387), yet turnover is 4.4x higher (2.101 vs 0.473) and the net spread is ~20x worse (−0.001705 vs −0.000085 at base cost; net is negative at 1x/2x/4x). Root cause is ridge scarcity: ridge bars/day median 10 (vs valley 205), so 48.5% of classifiable symbol-days fail the ≥10-ridge gate (557,428 of 1,149,313; 591,524 valid). That halves the cross-section (475 vs 929 names/day) and halves signal persistence (rank-autocorr half-life 8.0 vs 15.2 periods), so names churn in and out of the ranked universe. The IC survives; the tradability does not.

Note on the negative spread: unlike the sign=−1 factors, this is NOT affected by the frozen evaluator's aligned-spread cost-sign quirk — with sign=+1, aligned = net, so the reported figures are simply correct and bad. Independent sanity check: turnover 2.101 x fee 0.001 = 0.0021/period against a 0.0004/period gross spread.

Factor valley_ridge_vwap_ratio_20: reuses the merged peak taxonomy (ridge = eruptive & ~peak; an isolated peak is in NEITHER leg); per valid day, ratio = valley VWAP / ridge VWAP via the Σamount/Σvolume identity per leg (non-positive volume/amount bars dropped before summation); factor = mean daily ratio over the trailing 20 valid days. Day valid: ≥100 classifiable AND ≥20 tradable valley AND ≥10 tradable ridge AND positive denominators; NaN below 10 valid days. The asymmetric ridge floor is pinned and disclosed (spec + config + report description) with its rationale — at a 20-bar floor only 335,920 days would survive instead of 591,524. Pre-registered sign +1.

Honest caveats kept in the record: monotonicity is 0.90 not 1.0 (Q4 final NAV 1.667 slightly exceeds Q5's 1.607, so the top bucket is not the best performer); the median day sits exactly on the ridge threshold; 1 evaluation period has an empty cross-section and 1 symbol was dropped for missing minute coverage (both disclosed by the evaluator).

Test plan

  • Full pytest: 1473 passed (baseline 1421 + 52; PR-F/PR-H/PR-I tests untouched) — rerun independently by orchestrator
  • ruff clean; phase0 regression ic 0.9600 / annual 0.8408 unchanged; validate-config all pass
  • Independent adversarial review: APPROVE, no leak, no reuse drift. Reviewer monkeypatched the 14:50 truncation off and recomputed on 4 real symbols over 2022 — 100% of finite values changed, proving the cutoff is load-bearing. Separately extracted main's module via git show and compared all THREE prior factors (volume_peak_count_20, peak_interval_kurtosis_20, valley_relative_vwap_20) on 5 real symbols x ~58k rows each — bit-identical; the new ridge column verified disjoint from valley and peak on real data.
  • Hand-verified arithmetic re-derived by the reviewer (case A ratio 6/11, case B 135/88); the 9-vs-10 ridge boundary tests confirmed against the actual reused mask
  • Secret scan: 0 token values across diff, artifacts, log, and the tmp/ prototype record
  • LOW (non-blocking, noted): the ridge-scarcity numbers land in the run log / CLI / prototype file rather than inside the frozen report's own coverage section; the generic cross_section_size_mean (475.2 of a ~995 universe) corroborates the same magnitude. Folding factor-specific coverage into the report would need a change to the frozen layer.

The peak/ridge/valley classification already computed `eruptive` internally but
exposed only `classifiable` / `valley` / `peak`. Add `ridge` = eruptive & ~peak so
the ridge-price family (PR-J) consumes THE SAME classification instead of
re-deriving it and drifting apart from PR-F / PR-H / PR-I.

Purely additive: the three pre-existing masks are computed exactly as before and
no consumer reads columns positionally. The three masks now partition the
classifiable bars exactly (valley | peak | ridge == classifiable, pairwise
disjoint).

The rule is PINNED as the exact complement of the conservative peak test, so it is
wider than the literal 'eruptive next to an eruptive': it also covers
session-boundary eruptions and eruptions whose neighbour is unclassifiable, i.e.
every eruptive bar whose isolation is not provable.

Verified bit-identical on real cached minute data: volume_peak_count_20,
peak_interval_kurtosis_20 and valley_relative_vwap_20 over 30 CSI500 names x
2022-2024 (21142 symbol-days) give assert_frame_equal(check_exact=True) and the
same SHA256 b3e68795... before and after.
Reproduces the FOURTH factor of the Kaiyuan microstructure series #27 (谷岭加权价格比,
reportId 4957417 §7.1) as ValleyRidgeVwapRatioFactor and runs it through the frozen
StandardFactorEvaluator on real cached CSI500 data.

Direct robustness test of PR-I: same reused classification, same Sigma-amount/Sigma-volume
VWAP identity, same eval cell — only the DENOMINATOR changes, from the whole visible
day's VWAP to the RIDGE VWAP, so the two behavioural groups collide head-on.

Definition (PINNED, disclosed on the spec): daily ratio = valley VWAP / ridge VWAP,
averaged over the trailing 20 VALID days. A day is valid iff it has >=100 classifiable
bars, >=20 tradable valley bars, >=10 tradable ridge bars and positive volume in both
denominators. The ridge floor is deliberately LOWER because a ridge bar must erupt AND
fail the isolation test; the runner MEASURES and logs the realized ridge-bar
distribution, the day-validity rate and the counterfactual valid-day count at a 20-bar
floor, so the scarcity is a reported number rather than a hidden threshold.

Prototype sanity (tmp/design/pr_j_prototype_sanity.txt): mean daily cross-sectional
RankIC +0.0438 over 692 days on 30 CSI500 names, 2022-2024 — POSITIVE, matching the
pre-registered sign +1.

Full run (995/996 symbols, stk_mins_live_calls=0, 438s): both axes PASS as in PR-I —
no-book Watch (predictive PASS, IC 0.0339, ICIR 0.425, CI low +0.355, NW-t 14.11,
N_eff 854, win 66.8%, monotonicity 0.90); with-book Watch (incremental PASS,
incremental ICIR 0.289). Day validity 51.5% of classifiable symbol-days, materially
below PR-I's ~99.8% — disclosed, not hidden. Net long-short spread is NEGATIVE at
every cost scenario (gross +0.0004/period vs 2.10 turnover x 0.001 fee); reported
as-is, the frozen evaluator's known aligned-spread cost-sign issue is untouched.
@StackOverFlow11
StackOverFlow11 merged commit 9f473c3 into main Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant